﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Discovery;

namespace Nardax.ServiceModel.Discovery
{
    public class DiscoverableServiceProxy<TServiceContract> : IDiscoverableServiceProxy<TServiceContract>, IDisposable where TServiceContract : class
    {
        public static readonly Uri ScopeMatchByPartialPair = new Uri("http://schemas.nardax.com/2011/03/partialpair");

        private readonly List<ServiceProxy<TServiceContract>> _proxies;
        private readonly object _proxiesLock;
        private DiscoveryClient _discoveryClient;
        private ServiceHost _announcementServiceHost;
        private readonly object _discoveryClientUserState;

        public DiscoverableServiceProxy()
        {
            _proxies = new List<ServiceProxy<TServiceContract>>();
            _proxiesLock = new Object();
            Scopes = new Collection<Uri>();
            ScopeMatchBy = FindCriteria.ScopeMatchByPrefix;
            _discoveryClientUserState = new Object();
        }

        public void OpenAnnouncementService()
        {
            var announcementService = new AnnouncementService();
            announcementService.OfflineAnnouncementReceived += AnnouncementService_OfflineAnnouncementReceived;
            announcementService.OnlineAnnouncementReceived += AnnouncementService_OnlineAnnouncementReceived;

            _announcementServiceHost = new ServiceHost(announcementService);
            _announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
            _announcementServiceHost.Open();
        }

        private void AnnouncementService_OnlineAnnouncementReceived(object sender, AnnouncementEventArgs e)
        {
            AddEndpoint(e.EndpointDiscoveryMetadata);
        }

        private void AnnouncementService_OfflineAnnouncementReceived(object sender, AnnouncementEventArgs e)
        {
            RemoveEndpoint(e.EndpointDiscoveryMetadata);
        }

        public void CloseAnnouncementService()
        {
            _announcementServiceHost.Close();
            _announcementServiceHost = null;
        }

        public void DiscoverAvailableServices()
        {
            _discoveryClient = new DiscoveryClient(new UdpDiscoveryEndpoint());
            _discoveryClient.FindProgressChanged += DiscoveryClient_FindProgressChanged;
            _discoveryClient.FindCompleted += DiscoveryClient_FindCompleted;
            _discoveryClient.FindAsync(new FindCriteria());
        }

        private void DiscoveryClient_FindProgressChanged(object sender, FindProgressChangedEventArgs e)
        {
            AddEndpoint(e.EndpointDiscoveryMetadata);
        }

        private void DiscoveryClient_FindCompleted(object sender, FindCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                _discoveryClient.Close();
            }
            else
            {
                if (_discoveryClient.InnerChannel.State == CommunicationState.Opened)
                {
                    _discoveryClient.Close();
                }
            }

            _discoveryClient = null;
        }

        public void CancleServiceDiscovery()
        {
            _discoveryClient.CancelAsync(_discoveryClientUserState);
        }

        public void Call(Action<TServiceContract> method)
        {
            lock (_proxiesLock)
            {
                foreach (var proxy in _proxies)
                {
                    proxy.Call(method);
                }
            }
        }

        public IEnumerable<TResult> Call<TResult>(Func<TServiceContract, TResult> method)
        {
            lock (_proxiesLock)
            {
                foreach (var proxy in _proxies)
                {
                    yield return proxy.Call(method);
                }
            }
        }

        private void AddEndpoint(EndpointDiscoveryMetadata metadata)
        {
            if (IsFindCriteriaMatch(metadata))
            {
                lock (_proxiesLock)
                {
                    if (_proxies.Find(x => x.Address == metadata.Address) == null)
                    {
                        _proxies.Add(new ServiceProxy<TServiceContract>(metadata));
                    }
                }
            }
        }

        private void RemoveEndpoint(EndpointDiscoveryMetadata metadata)
        {
            lock (_proxiesLock)
            {
                var proxy = _proxies.Find(x => x.Address == metadata.Address);
                if (proxy != null)
                {
                    _proxies.Remove(proxy);
                }
            }
        }

        private bool IsFindCriteriaMatch(EndpointDiscoveryMetadata metadata)
        {
            var criteria = CreateFindCriteria();
            if (criteria.ScopeMatchBy.Equals(ScopeMatchByPartialPair))
            {
                return (from findCriteriaScope in criteria.Scopes
                        from serviceEndpointScope in metadata.Scopes
                        where serviceEndpointScope.AbsolutePath.MatchByPair(findCriteriaScope.AbsolutePath)
                        select serviceEndpointScope).Any();
            }

            return criteria.IsMatch(metadata);
        }

        private FindCriteria CreateFindCriteria()
        {
            var criteria = new FindCriteria(typeof(TServiceContract))
            {
                ScopeMatchBy = ScopeMatchBy
            };

            if (Scopes.Count > 0)
            {
                foreach (var scope in Scopes)
                {
                    criteria.Scopes.Add(scope);
                }
            }
            return criteria;
        }

        public Uri ScopeMatchBy { get; set; }

        public Collection<Uri> Scopes { get; private set; }

        public void Dispose()
        {
            Dispose(true);
        }

        ~DiscoverableServiceProxy()
        {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_announcementServiceHost != null)
                {
                    ((IDisposable)_announcementServiceHost).Dispose();
                }

                if (_discoveryClient != null)
                {
                    ((IDisposable)_discoveryClient).Dispose();
                }
            }
        }
    }
}
